/*
 * Copyright 2003-2007 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.googlecode.mvcguice.view;

import groovy.servlet.AbstractHttpServlet;
import groovy.text.Template;
import groovy.text.TemplateEngine;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Date;
import java.util.Map;
import java.util.WeakHashMap;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.codehaus.groovy.runtime.MethodClosure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.googlecode.mvcguice.tag.CoreTags;

/**
 * A generic servlet for serving (mostly HTML) templates.
 *
 * <p>
 * It delegates work to a <code>groovy.text.TemplateEngine</code> implementation
 * processing HTTP requests.
 *
 * <h4>Usage</h4>
 *
 * <code>helloworld.html</code> is a headless HTML-like template
 * <pre><code>
 *  &lt;html&gt;
 *    &lt;body&gt;
 *      &lt;% 3.times { %&gt;
 *        Hello World!
 *      &lt;% } %&gt;
 *      &lt;br&gt;
 *    &lt;/body&gt;
 *  &lt;/html&gt;
 * </code></pre>
 *
 * Minimal <code>web.xml</code> example serving HTML-like templates
 * <pre><code>
 * &lt;web-app&gt;
 *   &lt;servlet&gt;
 *     &lt;servlet-name&gt;template&lt;/servlet-name&gt;
 *     &lt;servlet-class&gt;groovy.servlet.TemplateServlet&lt;/servlet-class&gt;
 *   &lt;/servlet&gt;
 *   &lt;servlet-mapping&gt;
 *     &lt;servlet-name&gt;template&lt;/servlet-name&gt;
 *     &lt;url-pattern&gt;*.html&lt;/url-pattern&gt;
 *   &lt;/servlet-mapping&gt;
 * &lt;/web-app&gt;
 * </code></pre>
 *
 * <h4>Template engine configuration</h4>
 *
 * <p>
 * By default, the TemplateServer uses the {@link groovy.text.SimpleTemplateEngine}
 * which interprets JSP-like templates. The init parameter <code>template.engine</code>
 * defines the fully qualified class name of the template to use:
 * <pre>
 *   template.engine = [empty] - equals groovy.text.SimpleTemplateEngine
 *   template.engine = groovy.text.SimpleTemplateEngine
 *   template.engine = groovy.text.GStringTemplateEngine
 *   template.engine = groovy.text.XmlTemplateEngine
 * </pre>
 *
 * <h3>Servlet Init Parameters</h3>
 *
 * <h4>Logging and extra-output options</h4>
 *
 * <p>
 * This implementation provides a verbosity flag switching log statements.
 * The servlet init parameter name is:
 * <pre>
 *   generated.by = true(default) | false
 * </pre>
 *
 * <h4>Groovy Source Encoding Parameter</h4>
 *
 * <p>
 * The following servlet init parameter name can be used to specify the encoding TemplateServlet will use
 * to read the template groovy source files:
 * <pre>
 *   groovy.source.encoding
 * </pre>
 *
 * @see TemplateServlet#setVariables(ServletBinding)
 *
 * @author Christian Stein
 * @author Guillaume Laforge
 * @version 2.0
 */
public class TemplateServlet extends AbstractHttpServlet {
	private static final long serialVersionUID = 8333072389998699833L;

	private static final Logger LOGGER = LoggerFactory.getLogger(TemplateServlet.class);

	public static final String TEMPLATE_DIR = "/views";
	public static final String LAYOUT_TEMPLATE = "application_layout.html";

	/**
	 * Simple cache entry that validates against last modified and length
	 * attributes of the specified file.
	 *
	 * @author Christian Stein
	 */
	private static class TemplateCacheEntry {

		Date date;
		long hit;
		long lastModified;
		long length;
		Template template;

		public TemplateCacheEntry(File file, Template template, boolean timestamp) {
			if (file == null) {
				throw new NullPointerException("file");
			}
			if (template == null) {
				throw new NullPointerException("template");
			}
			if (timestamp) {
				this.date = new Date(System.currentTimeMillis());
			} else {
				this.date = null;
			}
			this.hit = 0;
			this.lastModified = file.lastModified();
			this.length = file.length();
			this.template = template;
		}

		/**
		 * Checks the passed file attributes against those cached ones.
		 *
		 * @param file
		 *  Other file handle to compare to the cached values.
		 * @return <code>true</code> if all measured values match, else <code>false</code>
		 */
		public boolean validate(File file) {
			if (file == null) {
				throw new NullPointerException("file");
			}
			if (file.lastModified() != this.lastModified) {
				return false;
			}
			if (file.length() != this.length) {
				return false;
			}
			hit++;
			return true;
		}

		@Override
		public String toString() {
			if (date == null) {
				return "Hit #" + hit;
			}
			return "Hit #" + hit + " since " + date;
		}

	}

	/**
	 * Simple file name to template cache map.
	 */
	private final Map<String, TemplateCacheEntry> cache;

	/**
	 * Underlying template engine used to evaluate template source files.
	 */
	private TemplateEngine engine;

	private String fileEncodingParamVal;

	private static final String GROOVY_SOURCE_ENCODING = "groovy.source.encoding";

	/**
	 * Create new TemplateSerlvet.
	 */
	public TemplateServlet() {
		this.cache = new WeakHashMap<String, TemplateCacheEntry>();
		this.engine = null; // assigned later by init()
		this.fileEncodingParamVal = null; // may be changed by init()
	}

	/**
	 * Gets the template created by the underlying engine parsing the request.
	 *
	 * <p>
	 * This method looks up a simple (weak) hash map for an existing template
	 * object that matches the source file. If the source file didn't change in
	 * length and its last modified stamp hasn't changed compared to a precompiled
	 * template object, this template is used. Otherwise, there is no or an
	 * invalid template object cache entry, a new one is created by the underlying
	 * template engine. This new instance is put to the cache for consecutive
	 * calls.
	 * </p>
	 *
	 * @return The template that will produce the response text.
	 * @param file
	 *            The HttpServletRequest.
	 * @throws ServletException
	 *            If the request specified an invalid template source file
	 */
	protected Template getTemplate(File file) throws ServletException {

		String key = file.getAbsolutePath();
		Template template = null;

		/*
		 * Test cache for a valid template bound to the key.
		 */
		LOGGER.debug("Looking for cached template by key \"{}\"", key);
		TemplateCacheEntry entry = cache.get(key);
		if (entry != null) {
			if (entry.validate(file)) {
				LOGGER.debug("Cache hit! {}", entry);
				template = entry.template;
			} else {
				LOGGER.debug("Cached template needs recompiliation!");
			}
		} else {
			LOGGER.debug("Cache miss.");
		}

		//
		// Template not cached or the source file changed - compile new template!
		//
		if (template == null) {
			LOGGER.debug("Creating new template from file {}...", file);

			String fileEncoding = (fileEncodingParamVal != null) ? fileEncodingParamVal :
					System.getProperty(GROOVY_SOURCE_ENCODING);

			Reader reader = null;
			try {
				reader = fileEncoding == null ? new FileReader(file) : new InputStreamReader(new FileInputStream(file),
						fileEncoding);
				template = engine.createTemplate(reader);
			} catch (Exception e) {
				throw new ServletException("Creation of template failed: " + e, e);
			} finally {
				if (reader != null) {
					try {
						reader.close();
					} catch (IOException ignore) {
						// e.printStackTrace();
					}
				}
			}
			cache.put(key, new TemplateCacheEntry(file, template, verbose));
			LOGGER.debug("Created and added template to cache. [key={}]", key);
		}

		//
		// Last sanity check.
		//
		if (template == null) {
			throw new ServletException("Template is null? Should not happen here!");
		}

		return template;

	}

	/**
	 * Initializes the servlet from hints the container passes.
	 * <p>
	 * Delegates to sub-init methods and parses the following parameters:
	 * <ul>
	 * <li> <tt>"generatedBy"</tt> : boolean, appends "Generated by ..." to the
	 *     HTML response text generated by this servlet.
	 *     </li>
	 * </ul>
	 * @param config
	 *  Passed by the servlet container.
	 * @throws ServletException
	 *  if this method encountered difficulties
	 *
	 * @see TemplateServlet#initTemplateEngine(ServletConfig)
	 */
	@Override
	public void init(ServletConfig config) throws ServletException {
		super.init(config);
		this.engine = initTemplateEngine(config);
		if (engine == null) {
			throw new ServletException("Template engine not instantiated.");
		}
		String value = config.getInitParameter(GROOVY_SOURCE_ENCODING);
		if (value != null) {
			this.fileEncodingParamVal = value;
		}
		LOGGER.info("Servlet {} initialized on {}", getClass().getName(), engine.getClass());
	}

	/**
	 * Creates the template engine.
	 *
	 * Called by {@link TemplateServlet#init(ServletConfig)} and returns just
	 * <code>new groovy.text.SimpleTemplateEngine()</code> if the init parameter
	 * <code>template.engine</code> is not set by the container configuration.
	 *
	 * @param config
	 *  Current serlvet configuration passed by the container.
	 *
	 * @return The underlying template engine or <code>null</code> on error.
	 */
	protected TemplateEngine initTemplateEngine(ServletConfig config) {
		String name = config.getInitParameter("template.engine");
		if (name == null) {
			return new SimpleTemplateEngine();
		}
		try {
			return (TemplateEngine) Class.forName(name).newInstance();
		} catch (InstantiationException e) {
			LOGGER.error("Could not instantiate template engine: {}", name, e);
		} catch (IllegalAccessException e) {
			LOGGER.error("Could not access template engine class: {}", name, e);
		} catch (ClassNotFoundException e) {
			LOGGER.error("Could not find template engine class: {}", name, e);
		}
		return null;
	}

	/**
	 * Services the request with a response.
	 * <p>
	 * First the request is parsed for the source file uri. If the specified file
	 * could not be found or can not be read an error message is sent as response.
	 *
	 * </p>
	 * @param request
	 *            The http request.
	 * @param response
	 *            The http response.
	 * @throws IOException
	 *            if an input or output error occurs while the servlet is
	 *            handling the HTTP request
	 * @throws ServletException
	 *            if the HTTP request cannot be handled
	 */
	@Override
	public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		LOGGER.debug("Creating/getting cached template...");

		//
		// Get the template source file handle.
		//
		File file = super.getScriptUriAsFile(request);
		String name = file.getName();
		if (!file.exists()) {
			response.sendError(HttpServletResponse.SC_NOT_FOUND);
			return; // throw new IOException(file.getAbsolutePath());
		}
		if (!file.canRead()) {
			response.sendError(HttpServletResponse.SC_FORBIDDEN, "Can not read \"" + name + "\"!");
			return; // throw new IOException(file.getAbsolutePath());
		}

		// Prepare the response buffer content type _before_ getting the writer.
		// and set status code to ok
		response.setCharacterEncoding(encoding);
		response.setContentType(CONTENT_TYPE_TEXT_HTML);
		response.setStatus(HttpServletResponse.SC_OK);

		final StringWriter templateOut = new StringWriter(1024);
		ServletBinding binding = new ServletBinding(request);
		render(file, templateOut, binding);
		final String templateContent = templateOut.toString();

		final PrintWriter out = response.getWriter();

		File layoutFile = new File(servletContext.getRealPath(TEMPLATE_DIR + '/' + LAYOUT_TEMPLATE)).getAbsoluteFile();
		if (layoutFile.exists() && layoutFile.canRead()) {
			binding.setVariable("BODY_CONTENT", templateContent);
			if (!binding.hasVariable("JAVASCRIPT")) {
				binding.setVariable("JAVASCRIPT", "");
			}
			if (!binding.hasVariable("STYLESHEET")) {
				binding.setVariable("STYLESHEET", "");
			}
			render(layoutFile, out, binding);
		} else {
			out.write(templateContent);
		}

		response.flushBuffer();
	}

	private void render(File file, Writer out, ServletBinding binding) throws ServletException, IOException {
		final String name = file.getName();

		long loadMillis = System.currentTimeMillis();
		Template template = getTemplate(file);
		loadMillis = System.currentTimeMillis() - loadMillis;

		//
		// Evaluate the template.
		//
		LOGGER.debug("Making template \"{}\"...", name);

		// Put out for tags
		binding.setVariable("out", out);
		setVariables(binding, out);

		long makeMillis = System.currentTimeMillis();
		template.make(binding.getVariables()).writeTo(out);
		makeMillis = System.currentTimeMillis() - makeMillis;

		LOGGER.debug("Template \"{}\" request responded. [create/get={} ms, make={} ms]",
				new Object[] { name, loadMillis, makeMillis });

	}

	private void setVariables(ServletBinding binding, Writer out) {
		binding.setVariable("out", out);
		{
			MethodClosure c = new TagMethodClosure(new CoreTags(), "formatDecimal", new PrintWriter(out));
			binding.setVariable("formatDecimal", c);
		}
	}

	private static class TagMethodClosure extends MethodClosure {
		private final PrintWriter out;

		public TagMethodClosure(Object owner, String method, PrintWriter out) {
			super(owner, method);
			this.out = out;
		}

		protected Object doCall(Object argument, Object closure) {
			System.out.println(argument);
			return super.doCall(new Object[] { argument, out });
		}
	}
}
